1 2 /* 3 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 4 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 5 Authors: Marcelo S. N. Mancini 6 7 Copyright Marcelo S. N. Mancini 2018 - 2021. 8 Distributed under the CC BY-4.0 License. 9 (See accompanying file LICENSE.txt or copy at 10 https://creativecommons.org/licenses/by/4.0/ 11 */ 12 module hip.game2d.sprite; 13 public import hip.api.renderer.texture; 14 public import hip.api.graphics.color; 15 public import hip.api.data.commons; 16 import hip.math.vector; 17 import hip.api; 18 import hip.game2d.renderer_data; 19 20 /** 21 * Encapsulates bunch of sprites to hold a contiguous list of vertices. 22 * It has some advantages than creating manually an array of similar sprites such as: 23 * - Copies only once to the SpriteBatch, which searches for the texture index once. 24 * - Boundary checks once 25 * - Makes the sprites array vertices linear, reducing cache misses. 26 * - Wraps the setTexture and draw process so no need to manually execute the foreach 27 */ 28 class HipMultiSprite 29 { 30 protected HipSpriteVertex[] vertices; 31 HipSprite[] sprites; 32 IHipTexture texture; 33 this(size_t spritesCount) 34 { 35 vertices = new HipSpriteVertex[4*spritesCount]; 36 sprites = new HipSprite[spritesCount]; 37 foreach(i; 0..spritesCount) 38 sprites[i] = new HipSprite(vertices[i*4..(i+1)*4]); 39 } 40 41 ref HipSprite opIndex(size_t index){return sprites[index];} 42 43 int opApply(scope int delegate(ref HipSprite) dg) 44 { 45 int result = 0; 46 foreach (item; sprites) 47 { 48 result = dg(item); 49 if (result) 50 break; 51 } 52 return result; 53 } 54 55 56 int opApply(scope int delegate(size_t index, ref HipSprite) dg) 57 { 58 int result = 0; 59 foreach (i, item; sprites) 60 { 61 result = dg(i, item); 62 if (result) 63 break; 64 } 65 return result; 66 } 67 68 69 void setTexture(IHipTexture texture) 70 { 71 this.texture = texture; 72 foreach(sp; sprites) 73 sp.setTexture(texture); 74 } 75 76 ref HipSpriteVertex[] getVertices() 77 { 78 //Vertices is already a data sink for the sprites, so no need to reassign. 79 foreach(i, sp; sprites) 80 sp.getVertices; 81 return vertices; 82 } 83 84 void draw() 85 { 86 import hip.api.graphics.g2d.renderer2d; 87 foreach(sp; sprites) 88 sp.isDirty = true; 89 drawSprite(texture, cast(ubyte[])getVertices()); 90 } 91 } 92 93 class HipSprite 94 { 95 IHipTextureRegion texture; 96 HipColor color = HipColor.white; 97 float x = 0, y = 0; 98 float scrollX = 0, scrollY = 0; 99 float rotation = 0; 100 //Tiling == 1 for consistency, 0 the image would disappear 101 float tilingX = 1, tilingY = 1; 102 float scaleX = 1, scaleY = 1; 103 104 float u1 = 0, v1 = 0, u2 = 0, v2 = 0; 105 106 ///Width of the texture region, (u2-u1) * texture.width 107 uint width; 108 ///Height of the texture region, (v2-v1) * texture.height 109 uint height; 110 111 private bool flippedX, flippedY; 112 113 protected bool isDirty = true; 114 protected HipSpriteVertex[] vertices; 115 116 package this(HipSpriteVertex[] sink) 117 { 118 this.vertices = sink; 119 setColor(HipColor.white); 120 } 121 122 this() 123 { 124 vertices = new HipSpriteVertex[4]; 125 setColor(HipColor.white); 126 setTexture(cast()HipDefaultAssets.getDefaultTexture()); 127 } 128 this(IHipAssetLoadTask task) 129 { 130 this(); 131 setTexture(task); 132 } 133 134 /** 135 * Synchronous texture loading based on a string. 136 */ 137 this(string texturePath) 138 { 139 this(HipAssetManager.createTextureRegion(HipAssetManager.loadTexture(texturePath).awaitAs!IHipTexture)); 140 } 141 142 this(IHipTexture texture) 143 { 144 vertices = new HipSpriteVertex[4]; 145 setTexture(texture); 146 } 147 this(IHipTextureRegion region) 148 { 149 vertices = new HipSpriteVertex[4]; 150 this.texture = region; 151 width = region.getWidth(); 152 height = region.getHeight(); 153 setRegion(region.getRegion()); 154 } 155 156 void setTexture(IHipTexture texture) 157 { 158 import hip.api; 159 this.texture = HipAssetManager.createTextureRegion(texture); 160 width = texture.getWidth; 161 height = texture.getHeight; 162 setRegion(this.texture.getRegion()); 163 } 164 void setTexture(IHipAssetLoadTask task) 165 { 166 HipAssetManager.addOnCompleteHandler(task, (asset) 167 { 168 this.setTexture(cast(IHipTexture)asset); 169 }); 170 } 171 172 final IHipTexture getTexture() { return texture.getTexture();} 173 final void setRegion(float u1, float v1, float u2, float v2) 174 { 175 setRegion(TextureCoordinatesQuad(u1,v1,u2,v2)); 176 } 177 void setRegion(IHipTextureRegion region) 178 { 179 width = region.getWidth(); 180 height = region.getHeight(); 181 texture = region; 182 setRegion(region.getRegion()); 183 } 184 void setRegion(TextureCoordinatesQuad c) 185 { 186 this.u1 = c.u1; 187 this.u2 = c.u2; 188 this.v1 = c.v1; 189 this.v2 = c.v2; 190 texture.setRegion(c.u1, c.v1, c.u2, c.v2); 191 const float[] v = texture.getVertices(); 192 193 vertices[0].vTexST = Vector2(v[0], v[1]); 194 vertices[1].vTexST = Vector2(v[2], v[3]); 195 vertices[2].vTexST = Vector2(v[4], v[5]); 196 vertices[3].vTexST = Vector2(v[6], v[7]); 197 if(flippedX) 198 { 199 flippedX = false; 200 setFlippedX(true); 201 } 202 if(flippedY) 203 { 204 flippedY = false; 205 setFlippedY(true); 206 } 207 } 208 209 void setPosition(float x, float y) 210 { 211 this.x = x; 212 this.y = y; 213 214 if(isDirty)return; 215 216 if(rotation != 0 || scaleX != 1 || scaleY != 1) 217 { 218 isDirty = true; 219 return; 220 } 221 222 float x2 = x+width; 223 float y2 = y+height; 224 225 //Top left 226 vertices[0].vPosition = Vector3(x, y,0); 227 228 //Top right 229 vertices[1].vPosition = Vector3(x2, y,0); 230 231 //Bot right 232 vertices[2].vPosition = Vector3(x2, y2,0); 233 234 //Bot left 235 vertices[3].vPosition = Vector3(x, y2,0); 236 } 237 238 ref HipSpriteVertex[] getVertices() 239 { 240 if(isDirty) 241 { 242 isDirty = false; 243 float _x = -cast(float)width/2 * scaleX; 244 float _y = -cast(float)height/2 * scaleY; 245 float x2 = _x+(width * scaleX); 246 float y2 = _y+(height * scaleY); 247 if(rotation == 0) 248 { 249 //Top left 250 vertices[0].vPosition = Vector3(_x+x, _y+y,0); 251 252 //Top right 253 vertices[1].vPosition = Vector3(x2+x, _y+y,0); 254 255 //Bot right 256 vertices[2].vPosition = Vector3(x2+x, y2+y,0); 257 258 //Bot left 259 vertices[3].vPosition = Vector3(_x+x, y2+y,0); 260 } 261 else 262 { 263 import core.math:sin,cos; 264 float c = cos(rotation); 265 float s = sin(rotation); 266 267 //Top left 268 vertices[0].vPosition = Vector3(c*_x - s*_y + this.x, c*_y + s*_x + this.y,0); 269 270 //Top right 271 vertices[1].vPosition = Vector3(c*x2 - s*_y + this.x, c*_y + s*x2 + this.y,0); 272 273 //Bot right 274 vertices[2].vPosition = Vector3(c*x2 - s*y2 + this.x, c*y2 + s*x2 + this.y,0); 275 276 //Bot left 277 vertices[3].vPosition = Vector3(c*_x - s*y2 + this.x, c*y2 + s*_x + this.y,0); 278 } 279 } 280 return vertices; 281 } 282 283 void setColor(HipColor color) 284 { 285 this.color = color; 286 vertices[0].vColor = color; 287 vertices[1].vColor = color; 288 vertices[2].vColor = color; 289 vertices[3].vColor = color; 290 } 291 292 void setScale(float scaleX, float scaleY) 293 { 294 this.scaleX = scaleX; 295 this.scaleY = scaleY; 296 isDirty = true; 297 } 298 void setRotation(float rotation) 299 { 300 import hip.math.utils; 301 this.rotation = rotation % (PI * 2); 302 isDirty = true; 303 } 304 ///Same thing as setRotation, but receives in Degrees 305 void setAngle(float angle) 306 { 307 import hip.math.utils:degToRad; 308 setRotation(degToRad(angle)); 309 } 310 311 int getWidth() const {return width;} 312 int getHeight() const {return height;} 313 int getTextureWidth() const {return texture.getTextureWidth();} 314 int getTextureHeight() const {return texture.getTextureHeight();} 315 316 /** 317 * This function is most useful for single images. For instance backgrounds, probably, if you have a 318 * texture atlas or a spritesheet, this function is not useful 319 */ 320 void setScroll(float x, float y) 321 { 322 setRegion( 323 -scrollX + x + u1, 324 -scrollY + y + v1, 325 -scrollX + x + u2, 326 -scrollY + y + v2 327 ); 328 scrollX = x; 329 scrollY = y; 330 } 331 332 void setFlippedX(bool flip) 333 { 334 if(flip != flippedX) 335 { 336 auto reg = texture.getRegion; 337 flippedX = flip; 338 vertices[0].vTexST.x = flip ? reg.u2 : reg.u1; 339 vertices[1].vTexST.x = flip ? reg.u1 : reg.u2; 340 vertices[2].vTexST.x = flip ? reg.u1 : reg.u2; 341 vertices[3].vTexST.x = flip ? reg.u2 : reg.u1; 342 } 343 } 344 void setFlippedY(bool flip) 345 { 346 if(flip != flippedY) 347 { 348 auto reg = texture.getRegion; 349 flippedY = flip; 350 vertices[0].vTexST.y = flip ? reg.v2 : reg.v1; 351 vertices[1].vTexST.y = flip ? reg.v2 : reg.v1; 352 vertices[2].vTexST.y = flip ? reg.v1 : reg.v2; 353 vertices[3].vTexST.y = flip ? reg.v1 : reg.v2; 354 } 355 } 356 bool isFlippedX() => flippedX; 357 bool isFlippedY() => flippedY; 358 359 360 /** 361 * Sets the tiling factor for this sprite. Default is 1. 362 */ 363 void setTiling(float x = 1, float y = 1) 364 { 365 assert(x != 0 && y != 0, "Tiling factor equals 0 will disappear the sprite image"); 366 367 setRegion( 368 u1 / tilingX * x, 369 v1 / tilingY * y, 370 u2 / tilingX * x, 371 v2 / tilingY * y 372 ); 373 tilingX = x; 374 tilingY = y; 375 } 376 377 void draw() 378 { 379 import hip.api.graphics.g2d.renderer2d; 380 this.isDirty = true; 381 drawSprite(texture.getTexture, cast(ubyte[])getVertices[]); 382 } 383 } 384 385 386 class HipSpriteAnimation : HipSprite 387 { 388 import hip.api.graphics.g2d.animation; 389 private IHipAnimation animation; 390 HipAnimationFrame* currentFrame; 391 392 this(){super();} 393 394 this(IHipAnimation anim) 395 { 396 super(); 397 animation = anim; 398 this.setAnimation(anim.getCurrentTrackName()); 399 } 400 401 IHipAnimationTrack getAnimation(string animName) 402 { 403 return animation.getTrack(animName); 404 } 405 /** 406 * Sets internal animation data. 407 */ 408 void setAnimation(IHipAnimation anim) 409 { 410 animation = anim; 411 setAnimation(animation.getCurrentTrackName()); 412 } 413 void setAnimation(string animName) 414 { 415 animation.play(animName); 416 setFrame(animation.getCurrentFrame()); 417 } 418 419 void setBounds(int width, int height) 420 { 421 this.width = width; 422 this.height = height; 423 } 424 425 void setFrame(HipAnimationFrame* frame) 426 { 427 this.currentFrame = frame; 428 this.texture = frame.region; 429 setBounds(frame.region.getWidth(), frame.region.getHeight()); 430 setRegion(texture.getRegion()); 431 } 432 433 void update(float dt) 434 { 435 animation.update(dt); 436 setFrame(animation.getCurrentFrame()); 437 } 438 }